Skip to content

Conversation

@binaryfire
Copy link
Contributor

Hi @albertcht. This isn't ready yet but I'm opening it as a draft so we can begin discussions and code reviews. The goal of this PR is to refactor Hypervel to be a fully standalone framework that is as close to 1:1 parity with Laravel as possible.

Why one large PR

Sorry about the size of this PR. I tried spreading things across multiple branches but it made my work a lot more difficult. This is effectively a framework refactor - the database package is tightly coupled to many other packages (collections, pagination, pool) as well as several support classes, so all these things need to be updated together. Splitting it across branches would mean each branch needs multiple temporary workarounds + would have failing tests until merged together, making review and CI impractical.

A single large, reviewable PR is less risky than a stack of dependent branches that can't pass CI independently.


Reasons for the refactor

1. Outdated Hyperf packages

It's been difficult to migrate existing Laravel projects to Hypervel because Hyperf's database packages are quite outdated. There are almost 100 missing methods, missing traits, it doesn't support nested transactions, there are old Laravel bugs which haven't been fixed (eg. JSON indices aren't handled correctly), coroutine safety issues (eg. model unguard(), withoutTouching()). Other packages like pagination, collections and support are outdated too. Stringable was missing a bunch of methods and traits, for example. There are just too many to PR to Hyperf at this point.

2. Faster framework development

We need to be able to move quickly and waiting for Hyperf maintainers to merge things adds a lot of friction to framework development. Decoupling means we don't need to work around things like PHP 8.4 compatibility while waiting for it to be added upstream. Hyperf's testing package uses PHPUnit 10 so we can't update to PHPUnit 13 (and Pest 4 in the skeleton) when it releases in a couple of weeks. v13 has the fix that allows RunTestsInCoroutine to work with newer PHPUnit versions. There are lots of examples like this.

3. Parity with Laravel

We need to avoid the same drift from Laravel that's happened with Hyperf since 2019. If we're not proactive with regularly merging Laravel updates every week we'll end up in the same situation. Having a 1:1 directory and code structure to Laravel whenever possible will make this much easier. Especially when using AI tools.

Most importantly, we need to make it easier for Laravel developers to use and contribute to the framework. That means following the same APIs and directory structures and only modifying code when there's a good reason to (coroutine safety, performance, type modernisation etc).

Right now the Hypervel codebase is confusing for both Laravel developers and AI tools:

  • Some classes use Hyperf classes directly, some extend them, some replace them. You need to check multiple places to see what methods are available
  • Some Hyperf methods have old (2019) Laravel signatures while some overridden ones have new ones
  • The classes are in different locations to Laravel (eg. there's no hypervel/contracts package, the Hyperf database code is split across 3 packages, the Hyperf pagination package is hyperf/paginator and not hyperf/pagination)
  • The tests dir structure and class names are different, making it hard to know what tests are missing when comparing them to Laravel's tests dir
  • There are big differences in the API (eg. static::registerCallback('creating') vs static::creating())
  • The mix of Hyperf ConfigProvider and Laravel ServiceProvider patterns across different packages is confusing for anyone who doesn't know Hyperf
  • There are big functional differences eg. no nested database transactions

This makes it difficult for Laravel developers to port over apps and to contribute to the framework.

4. AI

The above issues mean that AI needs a lot of guidance to understand the Hypervel codebase and generate Hypervel boilerplate. A few examples:

  • Models have trained extensively on Laravel code and expect things to have the same API. Generated boilerplate almost always contains incompatible Laravel-style code which means you have to constantly interrupt and guide them to the Hypervel-specific solutions.
  • Models get confused when they have to check both Hypervel and Hyperf dependencies. They start by searching for files in the same locations as Laravel (eg. hypervel/contracts for contracts) and then have to spend a lot of time grepping for things to find them.
  • The inheritance chain causes major problems. Models often search Hypervel classes for methods and won't remember to search the parent Hyperf classes as well.

And so on... This greatly limits the effectiveness of building Hypervel apps with AI. Unfortunately MCP docs servers and CLAUDE.md rules don't solve all these problems - LLMs aren't great at following instructions well and the sheer volume of Laravel data they've trained on means they always default to Laravel-style code. The only solution is 1:1 parity. Small improvements such as adding native type hints are fine - models can solve that kind of thing quickly from exception messages.


What changed so far

New packages

Package Purpose
hypervel/database Full illuminate/database port
hypervel/collections Full illuminate/collections port
hypervel/pagination Full illuminate/pagination port
hypervel/contracts Centralised cross-cutting contracts (same as illuminate/contracts)
hypervel/pool Connection pooling (internalised from hyperf/pool)
hypervel/macroable Moved Macroable to a separate package for Laravel parity

Removed Hyperf dependencies so far

  • hyperf/database
  • hyperf/database-pgsql
  • hyperf/database-sqlite
  • hyperf/db-connection
  • hyperf/collection
  • hyperf/stringable
  • hyperf/tappable
  • hyperf/macroable
  • hyperf/codec

Database package

The big task was porting the database package, making it coroutine safe, implementing performance improvements like static caching and modernising the types.

  • Ported from Laravel 12
  • Added 50+ missing methods: whereLike, whereNot, groupLimit, rawValue, soleValue, JSON operations, etc.
  • Full Schema builder parity including schema states
  • Complete migration system (commands are still using Hyperf generators for now)
  • Both Hyperf and Hypervel previously used global PHPStan ignores on database classes. I removed these and fixed the underlying issues - only legitimate "magic" things are ignored now.

Collections package

  • Full Laravel parity across all methods
  • Modernised with PHP 8.2+ native types
  • Full LazyCollection support
  • Proper generics for static analysis

Contracts package

  • Centralised cross-cutting interfaces (like illuminate/contracts)
  • Clean dependency boundaries - packages depend on contracts, not implementations
  • Enables proper dependency inversion across the framework

Support package

  • Removed hyperf/tappable, hyperf/stringable, hyperf/macroable, hyperf/codec dependencies
  • Freshly ported Str, Env and helper classes from Laravel
  • Consistent use of Hypervel\Context wrappers (will be porting hyperf/context soon)
  • Fixed some existing bugs (eg. Number::useCurrency() wasn't actually setting the currency)

Coroutine safety

  • withoutEvents(), withoutBroadcasting(), withoutTouching() now use Context instead of static properties
  • Added UnsetContextInTaskWorkerListener to clear database context in task workers
  • Added Connection::resetForPool() to prevent state leaks between coroutines
  • Made DatabaseTransactionsManager coroutine-safe
  • Comprehensive tests verify isolation

Benefits

  1. Laravel 1:1 API parity - code from Laravel docs works directly
  2. AI compatibility - AI assistants generate working Hypervel code
  3. Easier contributions - Laravel devs can contribute without learning Hyperf
  4. Reduced maintenance - fewer external dependencies to track and update
  5. Modern PHP - native types throughout, PHP 8.2+ patterns
  6. Better static analysis - proper generics and type hints
  7. Coroutine safety - verified isolation with comprehensive tests

Testing status so far

  • PHPStan passes at level 5
  • All existing tests pass
  • Coroutine isolation tests verify safety

What's left (WIP)

  • Port the remaining Hyperf dependencies
  • Port the full Laravel and Hyperf test suites
  • Documentation updates
  • Your feedback on architectural decisions

The refactor process

Hyperf's Swoole packages like pool, coroutine, context and http-server haven't changed in many years so porting these is straightforward. A lot of the code can be simplified since we don't need SWOW support. And we can still support the ecosystem by contributing any improvements we make back to Hyperf in separate PRs.

Eventually I'll refactor the bigger pieces like the container (contextual binding would be nice!) and the config system (completely drop ConfigProvider and move entirely to service providers). But those will be future PRs. For now the main refactors are the database layer, collections and support classes + the simple Hyperf packages. I'll just port the container and config packages as-is for now.


Let me know if you have any feedback, questions or suggestions. I'm happy to make any changes you want. I suggest we just work through this gradually, as an ongoing task over the next month or so. I'll continue working in this branch and ping you each time I add something new.

binaryfire and others added 30 commits January 23, 2026 10:22
- Add static $resolvedBuilderClasses property to Model for caching
- Update newEloquentBuilder to use cache (Hypervel optimization)
- Rename newModelBuilder to newEloquentBuilder in test (Laravel naming)
…eritance)

Laravel does NOT inherit ScopedBy attributes from parent classes.
PHP attributes are not inherited by default, and Laravel does not
implement custom inheritance logic. Updated tests to verify this
behavior is preserved (the old Hypervel added inheritance which
would be a Laravel API deviation).
In Hyperf containers, get() retrieves a bound instance while make()
creates a new one. Using make() bypassed mocked Application instances
in tests. This matches the old Hypervel behavior.
In Hypervel, make() always creates a fresh instance bypassing bindings,
while get() returns the bound singleton. Changed three locations:

- Factory::withFaker() - Faker Generator should be bound singleton
- ModelInspector::getObservers() - ModelListener holds stateful observer registrations
- ConnectionFactory::createConnector() - Honor user's explicit custom connector binding
Avoid unnecessary container access in bootHasEvents() when no
ObservedBy attributes exist. This matches the pre-port behavior
and allows models to be instantiated without a container when
they don't use observers.
…Test

- EventFake now implements Hypervel\Event\Contracts\Dispatcher instead of
  just Psr\EventDispatcherInterface, allowing it to be used with Model events
- Added missing interface methods: getListeners, hasWildcardListeners, getRawListeners
- Fixed method signatures to match interface (listen with priority, push with mixed)
- BroadcasterTest: Removed old Hyperf Booted::$container usage
- BroadcasterTest: Updated model stubs to properly override resolveRouteBinding
- BroadcasterTest: Assertions now verify Model instances with correct bound values
…nction

- Port helpers.php from Laravel with Hypervel namespaces
- Keep Swoole-specific environment() function
- Fix env() to call \Hypervel\Support\env() (namespaced function)
- Remove non-existent Env class reference
- Replace use Hyperf\Contract\Arrayable with Hypervel\Contracts\Support\Arrayable
- Replace use Hyperf\Contract\Jsonable with Hypervel\Contracts\Support\Jsonable
- Update PHPDoc FQCN references in Facades
- Added validateModelClass() to ensure registered model classes exist
  and extend Model (matches pre-port Hypervel behavior)
- Updated ModelListenerTest for new API (ContainerInterface + Dispatcher)
- Added tests for invalid model class and non-Model class registration
- Deleted ObserverManagerTest (ObserverManager class no longer exists,
  functionality moved to ModelListener.registerObserver)
- Created shared base class with migrateRefresh=true and custom migration path
- All Tmp integration tests now extend TmpIntegrationTestCase
- Fixes test pollution when running after Database tests (migrations weren't
  running because RefreshDatabaseState::$migrated was already true)
- Removed redundant getDatabaseDriver() and migrateFreshUsing() from child classes
- Updated TransactionManager class reference to DatabaseTransactionsManager
- Fixed testHaltingEventExecution to expect Laravel behavior (halt on first
  non-null response, not after any listener)
Create Foundation test stubs and migrations to avoid Workbench dependency:
- tests/Foundation/Stubs/User.php with model and factory
- tests/Foundation/migrations/ with users table migration
Use Hypervel\Database\ConnectionInterface instead of Hyperf\Contract\ConnectionInterface
- Add toJson() method to ApiResource (implements Jsonable interface)
- Cast int keys to string in Arr::set() for PHP 8 strict types
- Allow callable in Collection::where() signature
- Port Fluent from Laravel with modernized types
- Add Arrayable handling to PendingRequest::normalizeRequestOptions()
- Add class_map files to composer.json autoload for test compatibility
- Add newEloquentBuilder() override in HasNode trait to return custom QueryBuilder
- Fix Grammar::compileFrom() to accept Expression|string
- Fix Grammar::parameter() return type to string|int|float
- Fix test ordering by 'name' instead of non-existent 'title' column
- Add Postgres sequence reset after seeding with explicit IDs
The Hyperf package was binding db.connector.sqlite to its own connector,
overriding our ported SQLiteConnector.
- Add missing tearDown() with m::close() to prevent mock leakage
- Add missing setConnectionName() for afterCommit job tests
- Fix mock return values: insertGetId returns int, insert returns bool
- Fix TransactionManager -> DatabaseTransactionsManager class name
The Migrator::setConnection() can receive null and passes it to setSource(),
so the parameter must be nullable.
- Replace Str::from/fromAll with enum_value() function
- Change BackedEnum to UnitEnum for broader enum support
- Remove hardcoded 'testing' database connection from tests
- Add self-contained User model with factory for FrontendRequestsAreStatefulTest
- Add sanctum_test_users migration
Matches Laravel behavior - Arr::get() handles null gracefully by
returning the default value via the accessible() check. The previous
strict ArrayAccess|array type broke code passing null (e.g.,
LinkedInProvider passing $avatar from Arr::first() which can be null).
The Tmp directory contains temporary integration tests created during
development that require specific database setup and don't exist on main.
…on-rules

feat: port missing Laravel validation rules
- Fix helpers.php: add missing return null in optional(), use while loop in retry()
- Fix CacheCommandMutex: forceRelease() returns void, return true after
- Fix ProcessInspector: remove incorrect @var annotation
- Fix Onceable: add missing return null in tryFromTrace()
- Fix ModelWatcher: use ModelEvent instead of non-existent Event class
- Fix Response/CoreMiddleware/Logger: use toJson()/toArray() for Hypervel contracts
- Update ResponseTest to use Hypervel contracts with proper toJson() implementation
- Worker: Fix broken tap pattern, use until() like Laravel, update EventDispatcher type
- FailedJobProviders: Replace dynamic whereConnection/whereQueue with explicit where()
- HasFactory: Add phpstan ignore for optional $factory property check
- AutoScaler: Add type annotation to fix sum() type inference
- Horizon RedisQueue: Add type assertion for RedisJob in pop() callback
- Telescope migration: Fix getConnection return type to ?string
- Tests: Update event namespaces and Connection mock for database port
find() has explicit return type object|array|null in its method signature,
unlike first() which uses the generic TValue (stdClass) from BuildsQueries.
- Split static analysis into parallel jobs for src and types
- Use single PHP 8.4 version (type checking doesn't vary by PHP)
- Add composer caching (shares cache with other workflows)
- Update to actions/checkout@v6
- Remove @phpstan-ignore for PHP 8.4 lazy object methods (now recognized)
- Install gmp extension in tests workflow for GMP casting tests
- Add missing RequiresPhpExtension import so tests skip gracefully
  when gmp is unavailable (e.g., local development)
The reconnect test triggers PooledConnection's reconnector which logs
"Database connection refreshing." - this is expected behavior, not an error.
- Change Relation::get(), BelongsToMany::get(), HasOneOrManyThrough::get()
  return types from EloquentCollection to BaseCollection (Support\Collection)
- Change applyAfterQueryCallbacks() signature from mixed->mixed to Collection->Collection
- Move workbench migrations/factories/models to _tmp (tests should create own schema)
- Add "When Tests Expose Source Code Type Errors" section to porting guide
- Add Laravel integration tests (in progress, temporarily in _tmp)

The afterQuery callback mechanism allows replacing query results with a
different Collection subtype. Since Eloquent\Collection extends Support\Collection,
using the base type correctly covers both normal returns and afterQuery replacements.
- Port WithMigration attribute with type alias support ('cache', 'queue',
  'session' all map to 'laravel')
- Add testbench helper functions (after_resolving, load_migration_paths,
  default_migration_path, join_paths)
- Override Testbench TestCase::setUpTraits() to wrap database operations
  in setUpDatabaseRequirements(), ensuring WithMigration attributes are
  processed before migrations run
- Add workbench test migrations (users, cache, jobs, notifications tables)
- Add workbench cache config with database store
- Update AttributesTest for new WithMigration behavior
- Port DatabaseCacheStoreTest as first test using WithMigration
Move Laravel's database integration tests from _tmp staging area to
tests/Integration/Database/Laravel/ as the starting point for the
porting effort outlined in PLAN.md.

These tests are unmodified Laravel tests that will be ported one by
one to work with Hypervel's stricter typing and coroutine architecture.
DatabaseCacheStoreTest and DatabaseLockTest require cache package
methods that haven't been ported yet. Move to Todo/ with markTestSkipped
until the cache package is fully ported.

Missing methods:
- DatabaseStore: forgetIfExpired(), getConnection()
- Lock: isOwnedBy(), isOwnedByCurrentProcess() (public)
- DatabaseLock: getConnectionName()
Hypervel does not support the ::read/::write connection suffix syntax
that Laravel uses to force all queries to a specific PDO. This feature
is incompatible with Swoole's connection pooling architecture.

Changes:
- Remove parseConnectionName() from DatabaseManager
- Remove setPdoForType() from DatabaseManager
- Simplify configure() - no longer accepts $type parameter
- Remove $readWriteType property and setReadWriteType() from Connection
- Remove getNameWithReadWriteType() - replaced with getName()
- Simplify latestReadWriteTypeUsed() - returns $latestPdoTypeRetrieved

Normal read/write splitting still works:
- Configure read/write arrays in database config
- Automatic routing: reads go to readPdo, writes go to pdo
- Sticky sessions work (reads follow writes to primary)
- Transaction isolation works (all queries use primary)

Tests updated:
- DatabaseConnectionsTest: REMOVED comments for unsupported features
- DatabaseQueryExceptionTest: Changed mysql::read to mysql_replica
- Porting guide updated with removed test documentation pattern
The testing DatabaseConnectionResolver caches connections statically to
prevent pool exhaustion (since tests don't use defer() to release
connections). This caused test pollution where query logs, event
listeners, and other connection state leaked between tests.

Added resetCachedConnections() method that calls resetForPool() on all
cached connections, invoked at the start of each test in TestCase::setUp().

Also:
- Port DatabaseConnectionsTest for Hypervel (removed dynamic connection
  tests incompatible with Swoole pooling)
- Exclude unported Laravel integration tests from default test suite
- Fix BootTraitsTest missing method that AttributeParser reflects on
- Remove redundant empty() check in HandlesDatabases filter callback
- Add @method annotations to Testbench TestCase for trait-provided methods
  (refreshDatabase, runDatabaseMigrations, beginDatabaseTransaction, etc.)
- Apply CS-fixer formatting to unported Laravel integration tests
- Add verifyConfiguration() method to BcryptHasher and HashManager to
  support the 'hashed' cast type used by DatabaseCustomCastsTest
- Add Model::clearBootedModels() to DatabaseMigrations trait to prevent
  test pollution when Event::fake() creates a new EventFake (models
  must re-boot to register event callbacks with the current dispatcher)
- Port DatabaseCustomCastsTest with Hypervel namespaces and strict types
- Port DatabaseEloquentBroadcastingTest with Hypervel namespaces and
  updated channel name assertions to match Hypervel namespace
…delCustomCastingTest

- Update namespaces from Illuminate to Hypervel
- Add strict return types to afterRefreshingDatabase methods
- Update model property types (protected array $guarded, etc.)
- Update caster classes to match Hypervel's stricter interface signatures
- Fix Decimal test fixture to handle float/int values from arithmetic operations
…ToManyTest

- Update namespaces from Illuminate to Hypervel
- Add strict types to model properties and method signatures
- Fix sync() method to accept null (was missing from union type)
  - sync(), syncWithoutDetaching(), syncWithPivotValues() now accept null
  - Internal parseIds() already handled null correctly via (array) cast
- Update expected exception messages to use Hypervel namespace
…ectionLoadCountTest, EloquentCollectionLoadMissingTest and Fixtures

- Update namespaces from Illuminate to Hypervel
- Add strict types to model properties and method signatures
- Port Fixtures/User, Post, PostStringyKey, NamedScopeUser
- Update namespaces from Illuminate to Hypervel
- Add strict types to model properties and method signatures
- Update namespaces from Illuminate to Hypervel
- Add strict types to model properties and method signatures
- Update namespaces from Illuminate to Hypervel
- Add strict types to model properties and method signatures
- Update namespaces from Illuminate to Hypervel
- Add strict types to model properties and method signatures
- Adapt testItOnlyEagerLoadsRequiredModels for Hypervel's event dispatcher
  (wildcard listeners receive spread payload, not array)
- Comment out original test for restoration when illuminate/events is ported
- Update namespaces from Illuminate to Hypervel
- Add strict types to model properties and method signatures
- Remove event dispatcher mocking from MassPrunableTest (incompatible with
  Hypervel's event() helper) - functional assertions preserved
- Add TODO comments for event verification when illuminate/events is ported
- Add CallQueuedListener, Dispatcher classes
- Add QueueRoutes class to queue package
- Add ResolvesQueueRoutes trait to support package
- Update CallQueuedListener data PHPDoc to allow string|array
- Use bind() instead of singleton() in EventServiceProvider
- Add queue() method to Broadcasting Factory contract
- Add phpstan ignore for false positive in defer()
- Add listenersCache for prepared listeners (avoids creating new closures
  on every dispatch in long-running processes)
- Use Context for deferred events state (coroutine-safe, prevents race
  conditions when multiple coroutines defer events simultaneously)
- Invalidate listenersCache in listen(), setupWildcardListen(), forget()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change Breaking changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants